/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.web;

import com.google.inject.Inject;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.security.action.Secure;
import net.sourceforge.stripes.validation.SimpleError;
import net.sourceforge.stripes.validation.ValidationMethod;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.LogFactory;
import org.xptracker.domain.*;
import org.xptracker.domain.Queue;
import org.xptracker.persistence.PersistenceManager;
import org.xptracker.persistence.RollbackException;
import org.xptracker.services.EmailService;
import org.xptracker.services.EmailServiceException;
import org.xptracker.services.StoryState;
import org.xptracker.util.CollectionUtils;
import org.xptracker.util.IoUtil;
import org.xptracker.util.Predicate;
import org.xptracker.util.StringUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

@UrlBinding("/Story.action")
public class StoryActionBean extends StandardActionBean {
  private Story story;
  private Map<String, List<StoryCategory>> availableCategories;
  private List<Customer> availableCustomers;
  private List<User> availableTrackers;
  private List<Queue> openQueues;
  private List<String> categories;
  private List<String> customerIds;
  private List<String> trackerUsernames;
  private FileBean attachment;
  private String description;

  private String queueId;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  private EmailService emailer;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  private QueueNotifier queueNotifier;

  public Story getStory() {
    return story;
  }

  public void setStory(Story story) {
    this.story = story;
  }

  public FileBean getAttachment() {
    return attachment;
  }

  public void setAttachment(FileBean attachment) {
    this.attachment = attachment;
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public Map<String, List<StoryCategory>> getAvailableCategories() {
    return availableCategories;
  }

  public void setAvailableCategories(Map<String, List<StoryCategory>> availableCategories) {
    this.availableCategories = availableCategories;
  }

  public List<Customer> getAvailableCustomers() {
    return availableCustomers;
  }

  public void setAvailableCustomers(List<Customer> availableCustomers) {
    this.availableCustomers = availableCustomers;
  }


  public List<String> getCustomerIds() {
    return customerIds;
  }

  public void setCustomerIds(List<String> customerIds) {
    this.customerIds = customerIds;
  }

  public List<User> getAvailableTrackers() {
    return availableTrackers;
  }

  public void setAvailableTrackers(List<User> availableTrackers) {
    this.availableTrackers = availableTrackers;
  }

  public List<String> getTrackerUsernames() {
    return trackerUsernames;
  }

  public void setTrackerUsernames(List<String> trackerUsernames) {
    this.trackerUsernames = trackerUsernames;
  }

  public List<Queue> getOpenQueues() {
    return openQueues;
  }

  public void setOpenQueues(List<Queue> openQueues) {
    this.openQueues = openQueues;
  }

  public List<String> getCategories() {
    return categories;
  }

  public void setCategories(List<String> categories) {
    this.categories = categories;
  }

  public void setQueueId(String queueId) {
    this.queueId = queueId;
  }

  @DefaultHandler
  public Resolution storyForm() {
    final HttpServletRequest servletRequest = getContext().getRequest();
    story = loadStory();
    if (story == null) {
      story = new Story();
      servletRequest.setAttribute("type", "Create");
    } else {
      servletRequest.setAttribute("type", "Update");
    }
    return new ForwardResolution("/editor/editStory.jsp");
  }

  @Before(stages = LifecycleStage.BindingAndValidation)
  public void loadData() {
    this.availableCategories = DomainUtil.makeCategoryMap(domain.listCategories());

    this.availableCustomers = domain.listCustomers();
    final User currentUser = getCurrentUser();
    availableTrackers = new LinkedList<User>();
    availableTrackers.add(currentUser);
    availableTrackers.addAll(CollectionUtils.doFilter(domain.listUsers(), new Predicate<User>() {
      public boolean qualifies(User user) {
        return !currentUser.equals(user);
      }
    }));
    this.openQueues = domain.listOpenQueues();

    categories = new LinkedList<String>();
    customerIds = new LinkedList<String>();
    trackerUsernames = new LinkedList<String>();
  }

  @Secure(roles = "editor")
  @HandlesEvent("ShowAttachForm")
  public Resolution showAttachmentForm() {
    return displayStory(loadStory()).addParameter("destJsp", "/display/attachmentForm.jsp");
  }

  @HandlesEvent("ShowAttachList")
  public Resolution showAttachmentList() {
    return displayStory(loadStory()).addParameter("destJsp", "/display/attachmentList.jsp");
  }

  @Secure(roles = "editor")
  @HandlesEvent("Attach")
  public Resolution attachFile() {
    if (attachment != null) {
      if (StringUtils.isBlank(description)) {
        description = attachment.getFileName();
      }
      persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Object>() {
        public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
          final Story story = loadStory();
          try {
            receiver.insert(story.addFileAttachment(description, attachment.getFileName(), attachment.getContentType(),
                IoUtil.getByteArray(attachment.getInputStream())));
            addBlogEntry(receiver, story, "Added attachment: **" + attachment.getFileName() + "**");
            receiver.update(story);
          } catch (IOException e) {
            throw new RollbackException(e);
          }
          return null;
        }
      });
    }
    return new RedirectResolution("/display/interstitialAttachmentUpdate.jsp").addParameter("storyId",
        loadStory().getId());
  }

  //@TODO Move this to its own action bean?
  @HandlesEvent("DownloadAttachment")
  public Resolution downloadFile() {
    final FileAttachment file = domain.loadEntity(FileAttachment.class, param("fileId"));
    return new StreamingResolution(file.getContentType(), new ByteArrayInputStream(file.getContents())) {
      protected void stream(HttpServletResponse response) throws Exception {
        response.addHeader("Content-Disposition", "attachment; filename=\"" + file.getFilename() + "\"");
        super.stream(response);
      }
    };
  }

  @HandlesEvent("DeleteAttachment")
  public Resolution deleteFile() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {

      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        String fileId = param("fileId");
        FileAttachment attachment = story.removeAttachment(fileId);
        if (attachment != null) {
          receiver.remove(attachment);
          addBlogEntry(receiver, story, "Removed attachment: **" + attachment.getFilename() + "**");
          receiver.update(story);
        } else {
          throw new RollbackException("Unable to find attachment '" + fileId + "' on story '" + story + "'");
        }
        return displayStory(story).addParameter("destJsp", "/display/attachmentList.jsp");
      }
    });
  }

  @Secure(roles = "editor")
  @HandlesEvent("Create")
  public Resolution createStory() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        User user = getCurrentUser();
        story.setCreator(user);
        story.setCategories(nameToStoryCategory(categories));
        story.setCustomers(nameToCustomer(customerIds));
        story.getTrackers().addAll((domain.loadUsers(trackerUsernames)));
        StringBuilder message = new StringBuilder("Story created: '").append(story.getSummary()).append("'");
        receiver.insert(story);
        if (StringUtils.isNotBlank(queueId)) {
          Queue q = domain.loadEntity(Queue.class, queueId);
          new QueueSource(q).addStory(domain, receiver, queueNotifier, story);
          message.append('\n').append("Added to queue '").append(domain.loadEntity(Queue.class, queueId).getName()).append("'");
        }

        addBlogEntry(receiver, story, message.toString());
        return displayStory(story);
      }
    });
  }

  @ValidationMethod(on = {"Create", "Update"})
  public void validateStoryEdits() {
    if (StringUtils.isBlank(story.getSummary())) {
      getContext().getValidationErrors().add("story.summary", new SimpleError("Summary is required."));
    }
    getContext().getRequest().setAttribute("type", StringUtils.isBlank(story.getId()) ? "Create" : "Update");
  }

  @Secure(roles = "editor")
  @HandlesEvent("Update")
  public Resolution editStory() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        final Story toUpdate = loadEntity(Story.class, story.getId());
        List<String> changeLog = new ArrayList<String>();
        try {
          updateField(changeLog, "summary", toUpdate, story);
          updateField(changeLog, "severity", toUpdate, story);
          updateField(changeLog, "dueDate", toUpdate, story);
          updateField(changeLog, "estimate", toUpdate, story);
          updateField(changeLog, "descriptionContent.content", "description", toUpdate, story);
          updateField(changeLog, "size", toUpdate, story);
          updateField(changeLog, "descriptionContent.wikiStyle", "wikiStyle", toUpdate, story);
          if (changeLog.size() > 0) {
            addBlogEntry(receiver, toUpdate, "The following fields were changed:\n\n" + StringUtils.join(changeLog.iterator(), "\n"));
            receiver.update(toUpdate);
            getContext().getMessages().add(new SimpleMessage("Changes saved."));
          } else {
            getContext().getMessages().add(new SimpleMessage("No changes detected."));
          }
        } catch (Exception e) {
          LogFactory.getLog(getClass()).error("Unable to update story", e);
        }

        return displayStory(story);
      }
    });
  }

  private Set<Customer> nameToCustomer(List<String> customerIds) {
    return new HashSet<Customer>(CollectionUtils.map(customerIds, new CollectionUtils.Producer<String, Customer>() {
      public Customer produce(String from) {
        return domain.fetchEntity(Customer.class, from);
      }
    }));
  }

  private List<StoryCategory> nameToStoryCategory(List<String> categoryNames) {
    return CollectionUtils.map(categoryNames, new CollectionUtils.Producer<String, StoryCategory>() {
      public StoryCategory produce(String from) {
        return domain.loadCategory(from);
      }
    });
  }

  public void updateField(List<String> changeLog, String property, Story oldStory, Story newStory)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    updateField(changeLog, property, property, oldStory, newStory);
  }

  private void updateField(List<String> changeLog, String property, String friendlyPropertyName, Story oldStory,
                           Story newStory) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
    Object oldValue = PropertyUtils.getProperty(oldStory, property);
    Object newValue = PropertyUtils.getProperty(newStory, property);
    if (!ObjectUtils.equals(oldValue, newValue)) {
      changeLog.add(" * " + friendlyPropertyName);
      PropertyUtils.setProperty(oldStory, property, newValue);
    }
  }

  @Secure(roles = "editor")
  @HandlesEvent("AlterState")
  public Resolution alterState() {
    validateEntry();
    if (!getContext().getValidationErrors().isEmpty()) {
      return displayStory(loadStory());
    }

    try {
      persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Boolean>() {
        public Boolean doUpdate(PersistenceManager.UpdateReceiver receiver) {
          final Story story = loadStory();
          State nextState = State.valueOf(param("nextState"));
          String oldStateDescription = story.getState().getDescription();
          String nextStateDescription = nextState.getDescription();
          final StringBuilder blogEntry = new StringBuilder("State Changed from **").
              append(oldStateDescription).append("** to **").append(nextStateDescription).append("**\n");

          if (StoryState.setNextState(new StoryState.StoryStateVisitor() {
            public void setCurrentUser() {
              User user = getCurrentUser();
              story.changeOwner(user);
              story.addTracker(user);
            }

            public void removeOwner() {
              story.removeOwner();
            }

            public void addCurrentUserAsTracker() {
              story.addTracker(getCurrentUser());
            }

            public void removeFromQueue(String reason) {
              Queue queue = story.getQueue();
              if (queue != null) {
                int queueIndex = queue.remove(story);
                if (queueIndex >= 0) {
                  blogEntry.append("\n * Taken from queue **").append(queue.getName()).append("**, position **").
                      append(queueIndex).append("**");
                  queueNotifier.storyRemoved(queue, story, reason);
                }
              }
            }
          }, story, nextState, false)) {
            StoryActionBean.this.addBlogEntry(receiver, story,
                blogEntry.append("\n\n").append(param("entry")).toString());
            receiver.update(story);
            getContext().getMessages().add(new SimpleMessage("Story set to state '" + nextStateDescription + "'"));
            queueNotifier.sendNotifications(getCurrentUser());
          } else {
            getContext().getValidationErrors().add("Change State", new SimpleError("State '" + nextStateDescription + "' is not a valid next state."));
            throw new IllegalStateException();
          }
          return true;
        }
      });
    } catch (IllegalStateException e) {
      // do nothing -- this just breaks out of the transaction.
    }
    return displayStory(loadStory());
  }

  @HandlesEvent("AddTracker")
  public Resolution addTracker() {
    return toggleTracker(true);
  }

  @HandlesEvent("RemoveTracker")
  public Resolution removeTracker() {
    return toggleTracker(false);
  }

  @HandlesEvent("AddOtherTracker")
  public Resolution addAnotherTracker() {
    return toggleTracker(true, domain.loadEntity(User.class, param("userId")));
  }

  private Resolution toggleTracker(final boolean addTracker) {
    return toggleTracker(addTracker, getCurrentUser());
  }

  private Resolution toggleTracker(final boolean addTracker, final User user) {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        if (addTracker) {
          story.addTracker(user);
        } else {
          story.removeTracker(user);
        }
        receiver.update(story);
        return displayStory(story).addParameter("destJsp", "/display/trackerList.jsp");
      }
    });
  }

  @Secure(roles = "planner")
  @HandlesEvent("NewCustomer")
  public Resolution newCustomer() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        Customer customer = new Customer(param("customerName"));
        receiver.insert(customer);
        addCustomerToStory(receiver, story, customer);
        return listCustomersAjax(story);
      }
    });
  }

  public Resolution listCustomersAjax(Story story) {
    return displayStory(story).addParameter("destJsp", "/display/customerList.jsp");
  }

  private void addCustomerToStory(PersistenceManager.UpdateReceiver receiver, Story story,
                                  Customer customer) {
    story.addCustomer(customer);
    receiver.update(story);
  }

  @Secure(roles = "editor")
  @HandlesEvent("AddCustomer")
  public Resolution addCustomer() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        addCustomerToStory(receiver, story, loadEntity(Customer.class, param("uuid")));
        return listCustomersAjax(story);
      }
    });
  }

  @Secure(roles = "editor")
  @HandlesEvent("RemoveCustomer")
  public Resolution removeCustomer() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        Customer customer = loadEntity(Customer.class, param("uuid"));
        story.removeCustomer(customer);
        receiver.update(story);
        return listCustomersAjax(story);
      }
    });
  }

  private RedirectResolution displayStory(Story story) {
    return new RedirectResolution(DisplayActionBean.class).addParameter("id", story.getExtId());
  }

  @HandlesEvent("AddBlogEntry")
  public Resolution addBlogEntry() {
    validateEntry();
    if (!getContext().getValidationErrors().isEmpty()) {
      return displayStory(loadStory());
    }

    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = StoryActionBean.this.addBlogEntry(receiver, loadStory(), param("entry"));
        getContext().getMessages().add(new SimpleMessage("Blog entry added."));
        return displayStory(story);
      }
    });
  }

  @Secure(roles = "editor")
  @HandlesEvent("AddCategory")
  public Resolution addCategory() {
    return updateCategories(new Updater() {
      public void update(List<StoryCategory> categories, StoryCategory category) {
        categories.add(category);
      }
    });
  }

  @Secure(roles = "editor")
  @HandlesEvent("RemoveCategory")
  public Resolution removeCategory() {
    return updateCategories(new Updater() {
      public void update(List<StoryCategory> categories, StoryCategory category) {
        categories.remove(category);
      }
    });
  }

  @Secure(roles = "editor")
  @HandlesEvent("ReorderTasks")
  public Resolution reorderTasks() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        List<StoryTask> tasks = new ArrayList<StoryTask>(story.getTasks());
        story.getTasks().clear();
        receiver.update(story);
        story.setTasks(tasks);
        sortTaskOrder(buildTaskOrder(param("taskOrder")), tasks);
        receiver.update(story);
        return new StreamingResolution("text", new StringReader("true"));
      }
    });
  }

  private List<String> buildTaskOrder(String taskOrder) {
    List<String> neworder = new ArrayList<String>();
    String[] ordered = taskOrder.split("&");

    for (String order : ordered) {
      String[] ids = order.split("=");
      neworder.add(ids[1]);
    }
    return neworder;
  }

  private void sortTaskOrder(final List<String> newOrder, List<? extends Task> tasks) {
    Collections.sort(tasks, new Comparator<Task>() {
      public int compare(Task t1, Task t2) {
        Integer order1 = newOrder.indexOf(t1.getId());
        Integer order2 = newOrder.indexOf(t2.getId());
        return order1.compareTo(order2);
      }

      public boolean equals(Object obj) {
        return false;
      }
    });
  }

  private Resolution updateCategories(final Updater updater) {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        Story story = loadStory();
        StoryCategory category = loadEntity(StoryCategory.class, param("uuid"));
        updater.update(story.getCategories(), category);
        receiver.update(story);
        return displayStory(story).addParameter("destJsp", "/display/categoryList.jsp");
      }
    });
  }

  private static interface Updater {
    void update(List<StoryCategory> categories, StoryCategory category);
  }

  private Story addBlogEntry(PersistenceManager.UpdateReceiver receiver, Story story, String content) {
    return addBlogEntry(getCurrentUser(), story, content, this, receiver, emailer);
  }

  public static Story addBlogEntry(final User user, final Story story, final String content,
                                   StandardActionBean actionBean, final PersistenceManager.UpdateReceiver receiver,
                                   EmailService emailer) {
    final HttpServletRequest request = actionBean.getContext().getRequest();
    BlogEntry entry = new BlogEntry(user, story, content);
    receiver.insert(entry);
    story.getBlogEntries().add(entry);
    receiver.update(story);
    //todo Move to persistence listener framework?
    StringBuilder builder = new StringBuilder();
    int split = request.getRequestURL().lastIndexOf(request.getServletPath());
    String url = request.getRequestURL().substring(0, split);
    builder.append(url).append(DisplayActionBean.DISPLAY + "?id=").append(story.getExtId());
    builder.append("\n\n\n").append(StringUtil.truncate(story.getSummary(), 100));
    builder.append("\n\n\n").append(content);
    try {
      Set<User> recipients = new HashSet<User>(story.getTrackers());
      if (!user.getEmailMeMyChanges()) {
        recipients.remove(user);
      }
      emailer.sendEmail(user, recipients,
          "XPTracker - Change to Story " + story.getExtId(), builder.toString());
    } catch (EmailServiceException e) {
      actionBean.addGlobalError(e.getMessage());
    }
    return story;
  }

  private void validateEntry() {
    final String content = param("entry");
    if (StringUtils.isBlank(content)) {
      getContext().getValidationErrors().add("entry", new SimpleError("Blog entry is required"));
    }
  }

  private Story loadStory() {
    String storyId = param("storyId");
    return StringUtils.isBlank(storyId) ? null : loadEntity(Story.class, storyId);
  }
}
